function MapController($scope, $interval, $window) {

    var self = this;
    var updateTimer = null;
    var R = 6372795/1000;
    var stopDiameter = 0.020;
    var stopPoint;
    var id = 1;
    var lastPoint = [55, 37];
    var drawnPointsCount = 0;
    this.markersLayer = null;
    this.trackLayer = new L.FeatureGroup();
    this.speedLimits = {
        low: 60,
        normal: 80,
        color: ['red', 'yellow', 'green']
    };
    this.trackKey = null;
    this.track = new Track();

    this.init = function(mapDiv) {
        self.map = L.map(mapDiv).setView([55.505, 37.09], 13);
        //L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
        L.tileLayer('http://{s}.tiles.mapbox.com/v3/examples.map-i86nkdio/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
            maxZoom: 18
        }).addTo(self.map);
        self.map.addLayer(self.trackLayer);
        $scope.Math = window.Math;
        if($window.trackKey != null) {
            self.trackKey = $window.trackKey;
            self.loadMap();
        }
        $window.mapCtrl = self;
    };

    this.resetTrack = function() {
        self.track.key = self.trackKey;
        self.track.points = [];
        self.track.speed = 0;
        self.track.avgSpeed = 0;
        self.track.avgMoveSpeed = 0;
        self.track.distance = 0;
        self.track.time = 0;
        self.track.stopTime = 0;
        self.track.lastTime = null;
        self.track.finished = false;
        drawnPointsCount = 0;
    };

    /**
     * загружает карту по ключу трека
     */
    this.loadMap = function () {
        if(self.track.key != self.trackKey) {
            self.resetTrack();
        }
        if(self.trackKey != null) {
            //self.track.points = [[55.5, 37.09],[55.55, 37.095]];
            //var newPoints = self.generateTrackPoint();
            var newPoints = [];
            var lastPointId = 0;
            if(self.track.points.length > 0) {
                lastPointId = self.track.points[self.track.points.length - 1].id;
            }
            $.ajax({
                url: 'api/track/points/find?trackKey=' + self.trackKey + '&lastPointId=' + lastPointId,
                //data: { bookId: bookId },
                async: true,
                cache: false,
                success: function (data, status, xhr) {
                    console.info(data);
                    //self.tags.length = 0;
                    //var pList = jQuery.parseJSON(data);
                    //console.info(pList);
                    $.each(data, function (index, p) {
                        newPoints.push(new TrackPoint(p.id, p.latitude, p.longitude, Date.parse(p.time)));
                        //console.info(p.id, p.latitude, p.longitude, p.time);
                        //newPoints.push(new TrackPoint(p.id, [p.latitude, p.longitude], Date.now()));
                    });
                    console.info(newPoints);
                    $.merge(self.track.points, newPoints);
                    self.recalcTrackInfo(newPoints.length);
                    self.renderMap(false, newPoints.length);
                    $scope.$apply();
                    //$.each(response.items, function(index, tag) {
                    //    self.tags.push(new Tag(tag.id, tag.name, tag.frequency));
                    //});
                    //$.publish('tags-loaded', { items: self.tags });
                    //app.showNotification('Теги загружены', 1000);
                },
                error: function () {
                    //app.showError('Ошибка при загрузке тегов', 'Не удалось загрузить теги книги');
                }
            });
        } else {
            self.track.points = [];
        }
    };

    /**
     * Отрисовывает трек на карте
     */
    //todo: optimize
    this.renderMap = function () {
        var points = self.track.points;
        var trackLenght = points.length;
        var prevPoint = null;
        var polyline = [];
        if(drawnPointsCount == 0) {
            self.trackLayer.clearLayers();
        } else {
            prevPoint = points[drawnPointsCount - 1];
            polyline.push(prevPoint.geoPoint);
        }
        //$.each(self.track.points, function(index, trackPoint) {
        var trackPoint;
        var speedLevel = 0, lastSpeedLevel = -1;
        var dl = 0, dt = 0, v = 0;
        for(var index = drawnPointsCount; index < trackLenght; index++) {
            //console.info(index, trackPoint);
            trackPoint = points[index];
            if (prevPoint != null) {
                dl = self.distance(prevPoint.geoPoint, trackPoint.geoPoint);
                dt = (trackPoint.time - prevPoint.time) / (1000 * 3600);
                v = dl / dt;
                console.info('dl = ' + dl + '; dt = ' + dt + '; v = ' + v);
                speedLevel = v < self.speedLimits.low ? 0 : (v < self.speedLimits.normal ? 1 : 2);

                if (speedLevel != lastSpeedLevel && lastSpeedLevel >= 0) {
                    if (polyline.length > 1) {
                        self.trackLayer.addLayer(L.polyline(polyline, {color: self.speedLimits.color[lastSpeedLevel]}));
                    }
                    polyline = [];
                    polyline.push(prevPoint.geoPoint);
                }
                lastSpeedLevel = speedLevel;
            }
            prevPoint = trackPoint;
            polyline.push(trackPoint.geoPoint);
        }
        if(polyline.length > 1) {
            self.trackLayer.addLayer(L.polyline(polyline, {color: self.speedLimits.color[speedLevel]}));
        }
        drawnPointsCount = trackLenght;

        var center = [55.505, 37.09];
        if(trackLenght > 0) {
            center = points[trackLenght - 1].geoPoint;
        }
        self.map.panTo(center, 13);
        if(self.markersLayer == null) {
            self.markersLayer = new L.FeatureGroup();
            self.map.addLayer(self.markersLayer);
        } else {
            self.markersLayer.clearLayers();
        }
        self.markersLayer.addLayer(L.marker(center));
    };

    /**
     * включено ли автообновление карты
     * @returns {boolean}
     */
    this.isAutoupdate = function() {
        return updateTimer != null;
    };

    /**
     * включить автообновление карты
     */
    this.toggleAutoupdate = function() {
        console.info('toggleAutoupdate');
        if(updateTimer == null) {
            updateTimer = $interval(function () {
                self.loadMap();
            }, 3000);
        } else {
            $interval.cancel(updateTimer);
            updateTimer = null;
        }
    };

    /**
     * Расстояние между двумя точками на поверхности Земли
     * @param pt1 1я точка
     * @param pt2 2я точка
     * @returns {number}
     */
    this.distance = function(pt1, pt2) {
        var lat1 = Math.PI * pt1[0] / 180;
        var lat2 = Math.PI * pt2[0] / 180;
        var lng1 = Math.PI * pt1[1] / 180;
        var lng2 = Math.PI * pt2[1] / 180;
        return R * Math.acos(Math.cos(lat1) * Math.cos(lat2) * (Math.cos(lng1 - lng2) - 1) + Math.cos(lat1 - lat2));
    };

    /**
     * Пересчитывает параметры трека
     * @param newPointsCount
     */
    this.recalcTrackInfo = function() {
        var p0 = null, p1, p2, dt = 0, dl = 0;
        var N = self.track.points.length;
        var newPointsCount = N - drawnPointsCount;
        if(newPointsCount < N) {
            p1 = self.track.points[N - newPointsCount - 1];
        }
        for(var i = newPointsCount; i > 0; i--) {
            p2 = self.track.points[N - i];
            if(p1 != null) {
                dt = (p2.time - p1.time) / 1000;
                console.info('dt = ' + dt);
                self.track.time += dt;
                dl = self.distance(p1.geoPoint, p2.geoPoint);
                self.track.distance += dl;
                if(stopPoint != null) {
                    if(self.distance(p2.geoPoint, stopPoint.geoPoint) < stopDiameter) {
                        //self.track.stopTime = (p2.time - stopPoint.time) / 1000;
                        self.track.stopTime += dt;
                    } else {
                        stopPoint = null;
                    }
                } else if(dl < stopDiameter) {
                    stopPoint = p1;
                }
            }
            p0 = p1;
            p1 = p2;
        }
        if(newPointsCount > 0 && N > 1 && self.track.time > 0) {
            self.track.speed = (dt > 0)? 3600 * dl / dt: 0;
            self.track.avgSpeed = 3600 * self.track.distance / self.track.time;
            self.track.avgMoveSpeed = 3600 * self.track.distance / (self.track.time - self.track.stopTime);
        }
        if(N > 0) {
            self.track.lastTime = self.track.points[N - 1].time / 1000;
        }
        console.info('l = ' + self.track.distance + '; v = ' + self.track.speed + '; Vavg = ' + self.track.avgSpeed + '; Vmove = ' + self.track.avgMoveSpeed);
        console.info('t = ' + self.track.time + '; t0 = ' + self.track.stopTime + '; t1 = ' + (self.track.time - self.track.stopTime));
    };

    this.setSpeedLimits = function (low, normal) {
        console.log(low, normal);
        self.speedLimits.low = low;
        self.speedLimits.normal = normal;
        drawnPointsCount = 0;
        //self.recalcTrackInfo();
        self.renderMap();
    };

    /**
     * Генерирует путевую точку к юго-западу от предыдущей
     * @returns {*[]}
     */
    this.generateTrackPoint = function() {
        //lastPoint[0] -= (Math.ceil(id / 10) % 2 == 0? (1.5 + Math.random())/5000: (0.5 - Math.random())/15000);
        //lastPoint[1] -= (Math.ceil(id / 10) % 2 == 0? (1.5 + Math.random())/5000: (0.5 - Math.random())/15000);
        lastPoint[0] -= (1 + 2*Math.random())/5000;
        lastPoint[1] -= (1 + 2*Math.random())/5000;
        return [new TrackPoint(id++, lastPoint[0], lastPoint[1], Date.now())];
    };
}